package com.czy.project.springMVCframework.servlet;

import com.czy.project.springMVCframework.annotation.*;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * 入口类
 * 即spring中的DispatcherServlet
 * @author czy
 */
public class MyDispatcherServlet extends HttpServlet {

    private static final String LOCATION = "contextConfigLocation";//即web.xml中配置的init-param-name

    private Properties properties = new Properties();//用于保存配置文件application.properties的内容

    private List<String> classNames = new ArrayList<String>();//保存所有扫描出的全限定类名的集合

    private Map<String,Object> ioc = new HashMap<String,Object>();//ioc容器

    //保存所有的Url和方法的映射关系
    private List<Handler> handlerMapping = new ArrayList<Handler>();

    /**
     * 初始化一系列操作
     * @throws ServletException
     */
    @Override
    public void init(ServletConfig config) throws ServletException {
        //1.加载配置文件
        //根据contextConfigLocation名称从web.xml中获取出配置文件全称：application.properties

        String initParameter = config.getInitParameter(LOCATION);
        doLoadConfig(initParameter);

        /**
         * 2.扫描指定包下的类：spring中在application.xml中配置scanPackage指定扫描的包路径
         * 这里直接用application.properties代替
         */
        String scanPackage = properties.getProperty("scanPackage");//通过scanPackage键获取值，即获取要扫描的包路径
        doScanner(scanPackage);
        //3.初始化所有相关类的实例（这里就是初始化加@MyController，@MyService的类），并保存到IOC容器中
        doInstance();
        //4.依赖注入
        doAutowired();
        //5.构造HandlerMapping
        initHandlerMapping();
        //******************到此为止，spring相关ioc、di等初始化完成**********************
        //******************等待请求，匹配URL，定位方法， 反射调用执行*******************
        //******************调用doGet或者doPost方法*************************************
        System.out.println("MyDispatcherServlet[初始化完成]。。。等待调用。。。");
    }

    /**
     * doGet和doPost方法大家应该相当了解了
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req,resp);//doGet的操作交个doPost来执行
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try{
            doDispatch(req,resp); //开始匹配到对应的方法
        }catch (Exception e){
            //如果匹配过程出现异常，将异常信息打印出去
            resp.getWriter().write("500 Exception,Details:\r\n" + Arrays.toString(e.getStackTrace()).
                    replaceAll("\\[|\\]", "").
                    replaceAll(",\\s", "\r\n"));//将‘[’或者‘]’替换成"",将空白字符替换成换行符
        }

    }

    /**
     *匹配URL
     * @param req
     * @param resp
     */
    private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        Handler handler = getHandler(req);
        if (null == handler){
        //如果没有匹配上，返回404错误
            resp.getWriter().write("404 Not Found");//常见操作。。。
            return;
        }
        System.out.println("已匹配到处理器["+handler+"]");
        //获取方法的参数列表
        Class<?>[] paramTypes = handler.method.getParameterTypes();
        //保存所有需要自动赋值的参数值
        Object [] paramValues = new Object[paramTypes.length];
        Map<String,String[]> parameterMap = req.getParameterMap();//获取用户传入的参数map
        for (Map.Entry<String, String[]> param : parameterMap.entrySet()) {
            //获取所有参数即数组，并将数组转化成string，并替换[]为""、空白字符为","
            String value = new String(Arrays.toString(param.getValue()).getBytes("iso8859-1"),"utf-8").
                    replaceAll("\\[|\\]", "").replaceAll("\\s", ",");

            //如果handler的参数列表没有用户传入的key则跳出本次循环
            if (!handler.paramIndexMapping.containsKey(param.getKey()))continue;
            //找到匹配的参数，则开始填充参数值
            int index = handler.paramIndexMapping.get(param.getKey());//根据参数名获取参数对应的序号
            //url传过来的参数都是String类型的，HTTP是基于字符串协议
            //只需要把String转换为所需类型就好
            paramValues[index] = convert(paramTypes[index],value);
        }
        //设置方法中的request和response对象
        int reqIndex = handler.paramIndexMapping.get(HttpServletRequest.class.getName());
        paramValues[reqIndex] = req;
        int respIndex = handler.paramIndexMapping.get(HttpServletResponse.class.getName());
        paramValues[respIndex] = resp;
        //执行url对应的方法
        System.out.println("开始执行url对应的方法："+handler.method.getName());
        handler.method.invoke(handler.controller, paramValues);
    }

    /**
     *获取处理器Handler
     * @param request
     * @return
     */
    private Handler getHandler(HttpServletRequest request) {
        if (handlerMapping.isEmpty())return null;
        String url = request.getRequestURI();
        String contextPath = request.getContextPath();
        url = url.replace(contextPath,"").replaceAll("/+","/");

        for (Handler handler : handlerMapping) {
            Matcher matcher = handler.pattern.matcher(url);//根据用户的url获取一个匹配器matcher
            //matches方法用于全字符串匹配也就是100%匹配
            if (!matcher.matches())continue;//如果没有匹配到则跳过本次循环
            return handler;
        }
        return null;
    }

    /**
     * 初始化HandlerMapping（处理器映射器）：
     * HandlerMapping其实就是一个包含Handler的集合
     */
    private void initHandlerMapping() {
        if (ioc.isEmpty())return;
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {
            Class<?> clazz = entry.getValue().getClass();
            //如果该类上没有MyController注解则跳过本次循环
            if (!clazz.isAnnotationPresent(MyController.class))continue;
            String url = "";
            if (clazz.isAnnotationPresent(MyRequestMapping.class)){
                //如果类上有MyRequestMapping注解，获取Controller的url配置
                url =clazz.getAnnotation(MyRequestMapping.class).value();
            }

            //获取Method的url配置
            Method[] methods = clazz.getMethods();
            for (Method method : methods) {
                //没有加RequestMapping注解的直接忽略
                if(!method.isAnnotationPresent(MyRequestMapping.class))continue;
                //映射URL
                MyRequestMapping requestMapping = method.getAnnotation(MyRequestMapping.class);
                String regex =("/"+url+"/"+requestMapping.value()).replaceAll("/+","/");
                Pattern pattern = Pattern.compile(regex);
                handlerMapping.add(new Handler(entry.getValue(),method,pattern));
                System.out.println("HandlerMapping初始化完成【url：" + regex +"】" +"【method:" + method+"】");
            }
        }
    }

    /**
     * 依赖注入:就是拿到ioc容器中的类，然后访问类中的字段属性，是否包含Autowired注解
     * 然后从ioc容器中拿到实例并初始化该字段
     */
    private void doAutowired() {
        if (ioc.isEmpty())return;
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {
            //拿到实例对象中的所有属性
            Field[] fields = entry.getValue().getClass().getDeclaredFields();
            for (Field field : fields) {
                //如果该属性上没有MyAutowired注解则跳过本次循环
                if (!field.isAnnotationPresent(MyAutowired.class))continue;

                MyAutowired autowired = field.getAnnotation(MyAutowired.class);
                String beanName = autowired.value();
                if ("".equals(beanName)){//用户没有指定注入的beanName
                    beanName = field.getType().getName();//那么beanName就是该属性的名称
                }
                field.setAccessible(true);//暴力访问私有属性
                try {
                    field.set(entry.getValue(),ioc.get(beanName));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                    continue ;
                }
            }
        }
    }

    /**
     * 初始化所有相关类的实例，并保存到IOC容器中
     */
    private void doInstance() {
        if(classNames.size() == 0)return;
        //遍历全限定类名集合，拿到各类，并初始化
        try {
            for (String className : classNames) {
                Class<?> clazz = Class.forName(className);
                if (clazz.isAnnotationPresent(MyController.class)){ //如果类上声明了MyController注解
                   //将类名作为beanName，即ioc容器的key
                    String beanName = clazz.getName();
                    ioc.put(clazz.getName(),clazz.newInstance());
                    System.out.println("["+beanName+"]已添加到IOC容器");
                }else if (clazz.isAnnotationPresent(MyService.class)){//如果类上声明了MyService注解
                    MyService myService = clazz.getAnnotation(MyService.class);
                    String beanName = myService.value();//获取自定义的beanName
                    if (!"".equals(beanName.trim())){//如果用户设置了自定义的beanName，就用用户自己设置
                        ioc.put(beanName,clazz.newInstance());
                        System.out.println("["+beanName+"]已添加到IOC容器");
                        continue;
                    }
                    //如果自己没设，就按接口类型创建一个实例
                    Class<?>[] interfaces = clazz.getInterfaces();//获取该类实现的接口类
                    for (Class<?> c : interfaces) {
                        ioc.put(c.getName(),clazz.newInstance());
                        System.out.println("["+c.getName()+"]已添加到IOC容器");
                    }
                }else{
                    continue;
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 扫描指定包下的类
     * @param scanPackage:包扫描路径
     */
    private void doScanner(String scanPackage) {
        //将包路径转换为文件路径:即将com.czy.project.demo转化为 com/czy/project/demo
        String s = scanPackage.replace(".", "/");
        URL url = this.getClass().getClassLoader().getResource(s);
        File dir = new File(url.getFile());//获取该路径的文件
        //遍历该文件夹下的所有文件
        for (File file : dir.listFiles()) {
            if (file.isDirectory()){
                doScanner(scanPackage+"."+file.getName());//如果该文件是个文件夹，继续递归
            }else {
                //将全限定类名添加到集合
                classNames.add(scanPackage+"."+file.getName().replace(".class","").trim());
            }
        }
        for (String className : classNames) {
            System.out.println("已扫描到:["+className+"]");
        }
    }

    /**
     * 加载配置文件：application.properties
     * @param initParameter
     */
    private void doLoadConfig(String initParameter){
        //将application.properties加载到输入流
        try(InputStream is = this.getClass().getClassLoader().getResourceAsStream(initParameter)) {
            properties.load(is);//读取配置文件
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 内部类
     * Handler记录Controller中的RequestMapping和Method的对应关系
     * 即spring中一个url请求对应一个方法
     */
    private class Handler{
        protected Object controller;	//保存方法对应的实例
        protected Method method;		//保存映射的方法
        protected Pattern pattern;      //url对应的正则表达式,用于映射匹配
        protected Map<String,Integer> paramIndexMapping;	//参数名以及序号

        /**
         *构造一个Handler基本的参数
         * @param controller
         * @param method
         * @param pattern
         */
        public Handler(Object controller, Method method, Pattern pattern) {
            this.controller = controller;
            this.method = method;
            this.pattern = pattern;

            paramIndexMapping = new HashMap<String, Integer>();
            putParamIndexMapping(method);
        }

        /**
         * 处理方法中的参数
         * @param method
         */
        private void putParamIndexMapping(Method method) {
            //获取方法参数上的注解，注意Annotation是一个二维数组
            Annotation[][] p = method.getParameterAnnotations();
            for (int i = 0; i < p.length; i++) {
                for (Annotation a : p[i]) {
                    if (a instanceof MyRequestParam){//如果这个注解是MyRequestParam
                        String param = ((MyRequestParam) a).value();
                        if (!"".equals(param)){
                            paramIndexMapping.put(param,i);
                        }

                    }
                }
            }

            //提取方法中的request和response参数
            Class<?>[] parameterTypes = method.getParameterTypes();//获取所有参数的类型
            for (int i = 0; i < parameterTypes.length ; i ++) {
                Class<?> type = parameterTypes[i];
                if (type == HttpServletRequest.class ||
                        type == HttpServletResponse.class){
                    paramIndexMapping.put(type.getName(),i);
                }
            }
        }

        @Override
        public String toString() {
            return "Handler{" +
                    "controller=" + controller +
                    ", method=" + method +
                    ", pattern=" + pattern +
                    ", paramIndexMapping=" + paramIndexMapping +
                    '}';
        }
    }

    /**
     *  HTTP是基于字符串协议，所以url传过来的参数都是String类型的，
     *  只需要把String转换为对应方法中的类型就好
     * @param type
     * @param value
     * @return
     */
    private Object convert(Class<?> type,String value){
        if(Integer.class == type||type == int.class){
            return Integer.valueOf(value);
        }
        //如果还有double或者其他类型，继续加if
        //这时候，我们应该想到策略模式了
        return value;
    }

}
